CalcArray Documentation

How the code of the 2048 Power Compendium works

Note: If you've already read this before v3.0 and you're coming here from the Infused Mode Guide, Part 5 has the bulk of the information on Infused Modes. But if you haven't already read this documentation before, you should start at the beginning.

Part 1: Introduction to CalcArrays

Prelude

The 2048 Power Compendium is a collection of over 100 different gameplay variants of 2048, most of which were made by me (but some came from elsewhere or were suggested by players, though almost all of them were still coded, or at least re-implemented, by me). Several of the people on my Discord server have taken an interest in working with the code of the game (which is open-source and can be found on GitHub), so I figured I'd write a detailed explanation of how it works to aid them.

This blog post assumes you have some programming knowledge - the 2048 Power Compendium is coded in JavaScript, though by the nature of programming languages, if you know some other programming language you'll probably still be able to read this, seeing as JavaScript itself isn't what this post is about per se.

On the surface, the 2048 Power Compendium appears to be a collection of 2048 variants, but internally it's more like an engine for loading and running 2048 variants, which has 100 of them set to be loaded already. The code for loading a variant looks like this:

If the variants were being run directly in JavaScript, you'd expect something like if {Grid[y][x] == Grid[y][x + 1]} somewhere in there to check whether two tiles are equal. But instead of a bunch of if statements, there's a bunch of arrays. MergeRules and the last entry of TileTypes seem especially curious - what's up with "@This 0" and those mathematical expressions written as arrays?

That is what this blog post will explain.

(Before we begin, here's a warning: there are secrets hidden in the 2048 Power Compendium, and this blog post will give some spoilers for a couple of them. If you wish to find those secrets yourself, I recommend waiting to read this blog post until you do. If you do not know yet whether you have found all of the secrets, then you have not.)

What is a CalcArray?

The code that the 2048 Power Compendium runs to do things like merges is not written directly in JavaScript. It's written in CalcArrays, a sort of programming language within the Power Compendium's code.

In their most basic form, CalcArrays evaluate simple math operations. For example, [4, "+", 5] will evaluate to 9. If you're using the JS console on the Power Compendium website and wish to try out CalcArrays as we go, use the function CalcArray(); for example, CalcArray([4, "+", 5]); will return 9.

CalcArrays have no concept of an order of operations, they simply evaluate from left to right. For example, when evaluating [3, "+", 2, "*", 4], the addition is done first since it comes first in the array, so that array simplifies to [5, "*", 4], which evaluates to 20.

There is, however, an equivalent to parentheses: nested arrays. An array inside a CalcArray will itself be evaluated as a CalcArray, so [3, "+", [2, "*", 4]] becomes [3, "+", 8], which evaluates to 11.

Generally, when using an operator, the order of the arguments is argument 1, operator, argument 2, where often argument 1 is the result of everything up to that point. If an operator only has one argument, then it's just argument 1, operator. If an operator has more than two arguments, it's argument 1, operator, argument 2, argument 3, argument 4... up until the last one. All operators, with the exception of some special things that are moreso control tools than operators, have a fixed number of arguments.

CalcArrays can do a lot more than basic arithmetic, and numbers aren't the only data types that CalcArrays work with. They can also work with a few other types, such as strings, booleans, and bigints. Type conversion is automatic: each operator has a type it works with (with a few exceptions), and when it's time to apply that operator, its arguments will be automatically converted to that type before it's applied. CalcArrays can also do things like loops, conditionals, variables, and so on. We'll be going over all of those throughout this blog post.

Where and Why are CalcArrays used?

CalcArrays are used in a few places when creating a mode in the Power Compendium:

But why does the Power Compendium have a custom "programming language" for all these instead of just writing them in JavaScript? There are two main reasons:

We'll get to how to use CalcArrays in these places later on, but first we need to discuss how to use them on their own, because even when they're not attached to a tile type, merge rule, or other such structure, they can still be used as a programming language of sorts - at least in the console, anyway.

Part 2: CalcArray Operators

Number Operators

Let's start with the operators for numbers, since those were what CalcArrays were originally designed to work with. Here's a list of the operators that are generally used with numbers. Unless otherwise stated, the arguments to these operators are numbers, and the value they result in is a number.

Comparison Operators

These operators don't have a set type: they will take arguments of any type, so they won't do type conversion before evaluation.

Boolean Operators

These operators take boolean arguments and result in a boolean.

String Operators

The first argument to these operators is a string, but the later argument(s) might not be; unlike with number operators, where every argument was a number, string operators often do things that require some non-string arguments. These operators are generally denoted by having "str_" at the front, to distinguish them from their array counterparts if they have one.

Literal Arrays and Array Operators

Normally, when there's an array inside a CalcArray, that inside array is also interpreted as a CalcArray. If you want to create an actual array as a value to manipulate within a CalcArray, then you have to put "@Literal" at the beginning of that array. For example, putting [1, 5, 9] into a CalcArray will cause it to attempt to evaluate that as a CalcArray (and 5 isn't a valid operator, so that CalcArray will just result in 1), but if you put ["@Literal", 1, 5, 9] into a CalcArray, that will become the array [1, 5, 9] as an array value that can be used within the CalcArray.

Any array inside a literal array is also treated as a literal array; you do not need to put "@Literal" at the starts of the sub-arrays, only at the start of the external literal array. If you want an array inside a literal array to be evaluated as a CalcArray, put "@CalcArray" at the start of the array: for example, if you put ["@Literal", 1, [2, "+", 4], ["@CalcArray", 2, "+", 4]] into a CalcArray, then the value that evaluates to is the array [1, [2, "+", 4], 6].

CalcArrays treat arrays as values, not as objects. This means that, like strings, every CalcArray operation on an array creates a new array rather than mutating the old one, even operators like "arr_push" whose JavaScript equivalents mutate the existing array. This also means that checking arrays for equality doesn't check if they're "the same object", it checks if all their elements are equal. Inequalities still always result in false on arrays.

Here are the operators whose first argument is an array. These operators are generally denoted by having "arr_" at the front, to distinguish them from their string counterparts if they have one.

There's a few more advanced array operators that I can't explain yet, because they rely on variables, which I haven't discussed yet, to do their work: "arr_sort", "arr_map", "arr_filter", "arr_reduce", "arr_reduceRight", "arr_binarySearchCF", and "arr_binaryInsertCF". These will be discussed later.

BigInt Operators

Putting a BigInt into a CalcArray works differently depending on where you're doing it. If you're using CalcArrays in the Compendium directly (such as messing with the console or actually editing the code to make a mode), then they work just as you'd expect: the BigInt of value 5 is 5n. However, BigInts don't work nicely with JSON's stringify and parse methods, which are used for save codes and Infused Modes, so if you're editing a save code or making an Infused Mode, they're entered differently: the BigInt of value 5 is "@BigInt 5". These methods of entering BigInts are mutually exclusive: 5n won't work in a save code, and "@BigInt 5" won't work in the Compendium directly.

BigInt operators are generally the same as number operators, except they have a B on the end to indicate that they're the BigInt versions. The following operators work the same as their number counterparts, the only change is that their arguments and result are BigInts: "+B", "-B", "*B", "%B", "modB", "^B"/"**B", "absB", "signB", "gcdB", "lcmB", "factorialB", "primeB", "expomodB", "roundB" (remember, round has a second argument and it rounds the first argument to the nearest multiple of the second, so rounding isn't useless here), "floorB", "ceilB"/"ceilingB" (truncB currently doesn't exist, though I don't remember why I didn't add it), "bit&B", "bit|B", "bit~B", "bit<<B", "bit>>B", and "bit>>>B", and "rand_bigint" works the same as "rand_int".

"/B" and "logB" also exist. Since the number versions of these operators can result in decimals, their results are truncated here (remember, "truncated" means "rounded towards 0"): for example, [14n, "/B", 3n] results in 4n, since 14 / 3 is between 4 and 5. Truncated division is how division normally works with BigInts, so it shouldn't be a surprise that that's how it's done for division and logarithms here. Be careful here: CalcArrays do not have any error handling built in, so if you do something that cause BigInts to throw an error, like dividing by 0 or taking the logarithm of a negative number, the game will crash! (though "logB" will not throw an error when given 0n as its first argument; it will result in -1n instead).

"defaultAbbrevB" exists too. It's assumed that if you're using a BigInt, you care about the exact value of the number rather than just its size, so "defaultAbbrevB" never switches to scientific notation - the BigInt's digits are written out in full (with commas if it has at least five digits), regardless of how many digits there are.

The only number operators that don't have BigInt counterparts at all are the trig functions and "rand_float", since those four operators make no sense without non-whole numbers.

Several of the modes that do more advanced things with their numbers (1762, SQUART, X^Y, DIVE, and so on; basically any mode where the exact values of the numbers, rather than just multiples and products of powers of numbers, are relevant), use BigInts instead of numbers. This means that BigInts have some additional operators that do not have number equivalents:

These operators (aside from "rootB") don't have number versions because it's assumed that if you're in a situation that calls for such an operator, then you're in a situation where you care about the number's precise value, so you'd want to use BigInts anyway. (The only reason numbers even get "prime" is that that operator was added before BigInt support was added to CalcArrays. If that weren't the case, anything to do with primes would be BigInt exclusive, since if you're using numbers rather than BigInts you probably don't care about primes in that situation.)

BigRationals

In v2.1, a new number type was added to the 2048 Power Compendium: BigRational, exact-precision rational numbers, which are stored as a numerator and denominator that are both BigInts. BigRational is a class, so to make a BigRational with value 2/3, you do new BigRational(2n, 3n). You can also just give a single BigInt or number instead of two (if the argument is one number, continued fractions will be used to approximate a non-whole number as a fraction), or even a string such as "2/3", as an argument to the BigRational constructor. BigRationals are objects, but they're treated as if they're values: all of their methods create new BigRationals (so they do not mutate the existing ones). All BigRational operations automatically simplify their fractions, so new BigRational(1n, 3n).plus(new BigRational(1n, 6n)) will return a BigRational with value 1/2, i.e. with a numerator of 1 and a denominator of 2. BigRationals also support the three non-finite floating point values: 1/0 is Infinity, -1/0 is -Infinity (Infinity and -Infinity's numerators simplify to 1 and -1), and 0/0 is NaN (the existence of these allows for things like dividing by 0 that would cause BigInts to crash).

As with BigInts, you have to make BigRationals differently if you're editing a save code, since stringify doesn't preserve function methods: in a save code, "@BigRational 2 3" will make a BigRational with value 2/3.

Like how BigInt operators have B on the end, BigRational operators have BR on the end. The following operators are equivalents to number/BigInt operators that work as expected: "+BR", "-BR", "*BR", "/BR", "modBR" ("%BR" doesn't exist - since BigRational is a class I created, I only bothered to add the floored modulo since that's in my opinion the correct one, not the truncated modulo), "absBR", "signBR", "roundBR", "floorBR", "ceilBR"/"ceilingBR" (again, truncation isn't included here), "gcdBR", "lcmBR", "roundBR", "expomodBR" (unlike with numbers and BigInts, here the exponent can be negative: for example, 3/8 has -3 factors of 2 in it), and "perfectPowerFormBR".

"defaultAbbrevBR" writes BigRationals as mixed numbers.

There are also some BigRational operators that either don't have number or BigInt equivalents, or work differently than those equivalents:

v3.0 added a few more BigRational operators that attempt to include functions that aren't guaranteed to stay rational, with the twist that they always round the result to some rounding fraction (since their exact results can't be represented by BigRational):

BigRationals are used in a few places in the Power Compendium's code - places where both non-integer values and exact precision are needed. They're most visibly used in the Partial Absorb variant of the mode 180, but they're also used for the colors of tiles in mod 27, calculating the ratio between tiles in 3385, and a few other places.

GaussianBigInts

This is the other number type class that the Compendium uses that's not a native JS type. GaussianBigInts were actually added before BigRationals, in v1.5, but I chose to mention BigRationals first since they're easier to understand and are a better example of these classes. A GaussianBigInt represents a Gaussian integer, a complex number of the form a+bi for integers a and b. As with BigRationals, you'd make a GaussianBigInt with value 2+3i via new GaussianBigInt(2n, 3n), unless you're in a save code, in which case you'd do "@GaussianBigInt 2 3" instead.

The complex numbers aren't ordered, so inequalities don't work on them: inequalities on GaussianBigInts will always result in false, max/min on GaussianBigInts will throw an error.

GaussianBigInt operators have GB on the end. The following operators are equivalents to operators from the other number classes and work as expected: "+GB", "-GB", "*GB", "modGB", "negGB", "gcdGB", "lcmGB", "expomodGB", and "defaultAbbrevGB".

The kind of number that GaussianBigInts are representing is more "exotic" than the kind that BigRationals are representing, so there are more operators here that work differently:

GaussianBigInt isn't as versatile as BigRational, so as of now it's only used in one place in the 2048 Power Compendium: in the waves mode Gaussian DIVE.

Other Operators

And now for the operators that don't fall under one of the above categories.

First, there's the type conversion operators, which all take 1 argument: "Number", "String", "Boolean", "Array", "BigInt", "GaussianBigInt", and "BigRational", which each convert the argument into their respective type. "Number" on a GaussianBigInt will result in 0 unless the GaussianBigInt is pure real, while "Number" on a BigRational works properly (i.e. converting the BigRational 3/2 to a number gives the number 1.5). Unlike in JavaScript, arrays converted to strings in CalcArrays will include the brackets on the edges of the array. A GaussianBigInt converted into a string will be written as "a+bi" or "a-bi" where a and b are the component numbers. A BigRational converted into a string will be written as a (potentially improper) fraction, unless the denominator is 1 (in which case it's written as a whole number) or 0 (in which case it's written as "Infinity", "-Infinity", or "NaN"). "Boolean" on a GaussianBigInt or BigRational will always result in true, since they're technically objects. "Array" results in a one-element array where the element is the argument (unlike the implicit type conversion, the "Array" operator does this even if that element was itself an array). "BigInt" will, if the value can't be converted to a BigInt, try rounding it first, and if it still fails (such as if you're converting a string, not a number, and that string can't become a BigInt itself), will default to 0. Likewise, "GaussianBigInt" will default to 0+0i if the argument can't be converted correctly, while "BigRational" will default to 0/0 (NaN) if the argument can't be converted correctly.

There's also "typeof", which takes 1 argument and results in the type of that argument as a string: "number", "string", "boolean", "array", "bigint", "gaussianbigint", or "bigrational". (CalcArrays don't have tools built in to handle the value undefined, so it's recommended to avoid writing CalcArrays that could get undefined involved, but if it shows up anyway, "typeof" will still result in "undefined" on it.)

There's a couple simple ones that just result in one of the arguments, ignoring the other one:

"announce", "output", and "console.log" all display their second argument somehow then result in their first argument. "announce" displays the second argument as a message on the screen; this is how DIVE shows its messages about seed unlocks and eliminations. "announce" takes three arguments; the third argument is how many milliseconds the announcement lasts for. The other two only take two arguments. "output" converts the second argument to a string and places it as a text element at the bottom of the page, while "console.log" logs the second argument to the console. "output" and "console.log" should only be used for testing/bugfixing, not in a finished mode.

"defaultAbbrevAny" is a typeless version of the other defaultAbbrev operators, which will call one of the four of those depending on the type of the argument (it will leave the argument unchanged if it's not one of the four numeric types). Remember that this will always result in a string for BigInts, BigRationals, and GaussianBigInts, but it might result in a string or a text expression for numbers because of scientific notation having a superscript.

"@primesUpdate" (2 arguments) updates the array of prime numbers that the Compendium currently has stored so that it contains every prime up to at least the second argument, and then it results in the first argument (so it leaves the CalcArray's running value unchanged).

"multicolor" (2 arguments) is an operator used in the implementation of Multicolor Tiles. The first argument is an array of numbers, and the second argument is the amount of colors. The result is the "next" number in the cycle, or -1 if the array doesn't fit the multicolor tiles cycle requirement.

There's a couple operators that evaluate CalcArrays within a CalcArray. "CalcArray" takes 1 argument, a literal array, and evaluates it as a CalcArray. "CalcArrayParent" takes 2 arguments, where the first is of any type and the second is a literal array, and "applies" the second argument as a CalcArray to the first argument, i.e. evaluates a CalcArray that's the second argument but with the first argument unshifted onto the second argument as its first element. For example, [1, "CalcArrayParent", ["@Literal", "+", 2]] applies the ["+", 2] as if it's a function with 1 as the input, so it evaluates [1, "+", 2], resulting in 3.

DIVE turned out to be a complicated enough mode that it needed an operator added specifically for it: "DIVESeedUnlock", which takes three arguments. "DIVESeedUnlock"'s first argument is a bigint, its second argument is a list of bigints, and its third argument is a number (either 0, 1, 2, 3, or 4). "DIVESeedUnlock" checks the first argument as a new tile being made in DIVE to see if it would unlock a new seed. The second argument is the list of existing seeds to try dividing it by. The resulting value is what new seed would be unlocked (if it results in 1n, that means no unlock, since the first argument can be fully divided by the existing seeds). The third argument is the mode to do the checks in: mode 0 checks the seeds largest to smallest, mode 1 uses the recursive algorithm the original DIVE uses that ensures the minimum possible outcome (but this theoretically runs in exponential, or perhaps factorial, time with respect to the number of seeds, so it could get quite laggy, though you shouldn't run into lag with it in a normal DIVE game), mode 2 checks the seeds smallest to largest, mode 3 checks them in the order they already are in the list, and mode 4 uses the recursive algorithm but to ensure the maximum possible outcome (while still obeying the "can't be divided by any of the seeds anymore" rule) instead of the minimum. Many of the waves modes use "DIVESeedUnlock", but Gaussian DIVE needed its own variant: "GaussianDIVESeedUnlock", which is like "DIVESeedUnlock" but using GaussianBigInts instead of BigInts; "GaussianDIVESeedUnlock" takes four arguments, with the fourth being a boolean that determines whether the result should be rotated into the first quadrant (if true) or left as is (if false). There's a third one of these, "CustomDIVESeedUnlock", which I will discuss later once we've learned about variables.

Part 3: Other CalcArray Features

Conditionals and Loops

The string "@if" is placed in a CalcArray in the position that an operator would be, but it's not considered an operator itself. "@if" creates an if statement: the next entry of the CalcArray after the "@if" should be a CalcArray that would result in a boolean, and then the terms after that (the first of which should be an operator itself, and continue the CalcArray from there) will only be applied if that boolean CalcArray expression resulted in true. The if statement lasts until an "@end-if" is reached.

For example, in [3, "@if", ["@This 0, "=", 4], "*", 4, "+", 2, "@end-if", "-", 1], if ["@This 0, "=", 4] results in true (more on what "@This 0" means later), then the *4 and the +2 will be applied, so the CalcArray will result in 13. If ["@This 0, "=", 4] results in false, then the *4 and the +2 will be skipped since they're inside the if statement, but the -1 will still be applied, so the CalcArray will result in 2.

To go along with "@if", there's also "@else" and "@else-if". Each of these works similarly to "@if" in that the terms after them are considered part of their statement until an "@end-else" or "@end-else-if" (respectively) is reached. An "@else" or "@else-if" statement will be skipped unless the most recent "@if" or "@else-if" statement in this CalcArray had its boolean expression result in false, and there hasn't been another "@else-if" or "@else" checked since the most recent false-resulting "@if" or "@else-if" check. An "@else" statement does not have a boolean-resulting CalcArray after the "@else": if the most recent "@if" or "@else-if" boolean resulted in false, the "@else" statement is definitely triggered, and the first term after the "@else" should be the operator that starts the "@else" statement. "@else-if", like "@if", does have a boolean CalcArray as the first element after the "@else-if", so for an "@else-if" statement to be applied, the most recent "@if" or "@else-if"'s boolean expression must have resulted in false, and the "@else-if"'s boolean expression must result in true.

To make loops, you use "@repeat". The term after a "@repeat" should be either a number, or a CalcArray that results in a boolean (if it's a number it needs to be a plain number, not a CalcArray that results in a number), and the terms after that is the CalcArray segment to apply repeatedly; the end of a looping segment is denoted by "@end-repeat". If the term after the "@repeat" is a number, then that number is the amount of times that the loop is run. If the term after the "@repeat" is a CalcArray, then that CalcArray will be run before each loop, and the loop will only continue if that CalcArray results in true.

Of course, these conditional and loop statements can be nested inside each other - so make sure you put your "@end-if"s, "@end-repeat"s, etc. in the right places, or things will get buggy! If you need to exit all your statements at once, there's "@end-stack", which marks the end of all ifs, elses, else-ifs, and repeats it's inside at once (though in the repeat case it doesn't forcefully end it, the loop will keep going until it ends as usual). "@end-stack" is currently unused in the Compendium, and I don't foresee it being used anytime soon, so it's currently pretty much untested - so it might not work anyway.

Parents

Strings with an @ at the beginning of them tend to do special things in a CalcArray. These can be special operator-like tools, like conditionals and loops, but they can also be stand-ins for values that will be evaluated when it comes time to evaluate them. "@Parent" strings are one example of this.

An "@Parent" string is a way to reference the running value of the current CalcArray or of one of the parent CalcArrays it's inside. "@Parent -1" will, at evaluation time, be replaced with whatever the running value of the current CalcArray is. "@Parent -2" will be replaced with the running value of the CalcArray that the current CalcArray is inside (if one exists), "@Parent -3" will be replaced with the running value of the CalcArray two layers up, and so on. These work like the indexes in the .at() method for arrays, so while negatives go from the inside out, positives go from the outside in: "@Parent 0" refers to the running value of the outermost CalcArray that this CalcArray is in some nested layer of, "@Parent 1" to one layer within that, and so on. The nonnegative indices are currently unused in the Compendium - the negative indices are much more useful, since their behavior is less dependent on how many layers deep in CalcArrays they're in.

As an example, take [3, "+", 8, "*", [2, "+", "@Parent -2"], "-", 7]. The 3+8 is evaluated first, turning it into [11, "*", [2, "+", "@Parent -2"], "-", 7]. Now the inner CalcArray is evaluated, and the "@Parent -2" refers to the running value of the CalcArray outside the inner one, which in this case is 11, so it becomes [11, "*", [2, "+", 11], "-", 7], which becomes [11, "*", 13, "-", 7], which becomes [143, "-", 7], and thus the result is 136.

Replacing an @Parent string with the appropriate value does not happen until the operator where the @Parent string is an argument is reached in the CalcArray's process. The replacement is not permanent: if this is inside a loop, then the @Parent will be re-evaluated each time it's reached.

Variables

Normally, the only changing value that a CalcArray stores is its "running value" (its current first argument), as well as being able to access the running values of its parent CalcArrays via @Parent strings. But those aren't the only changeable values that a CalcArray can work with: you can also add changeable variables into a CalcArray and work with those.

Variables in a CalcArray are stored in an array that the CalcArray works with internally. By default, this array is empty. The typical way to add variables to a CalcArray is at the start, before the CalcArray begins properly. To do this, begin the CalcArray by having its first few elements be the variables, then put in "@end_vars", and then have the proper CalcArray part from there. For example, the CalcArray [3, "aaa", true, 8, "@end_vars", 4, "+", 5] will have [3, "aaa", true, 8] as its array of variables, then it will evaluate [4, "+", 5] and result in 9.

Of course, variables are useless if you don't access them. To access a variable, use an "@Var" string. For example "@Var 0" becomes the variable at index 0 of the variables array, "@Var 1" becomes the variable at index 1 of the variables array, "@Var -1" becomes the last variable of the variables array, "@Var -2" becomes the second-to-last variable of the variables array, and so on. For example, in [1, 2, 3, 4, "@end_vars", 5, "*", "@Var 2"], the variables array becomes [1, 2, 3, 4], and then it evaluates [5, "*", "@Var 2"]; the variable at index 2 is 3, so this becomes [5, "*", 3] and results in 15. As with @Parent strings, @Var strings are only replaced with a value when it's time to evaluate them, and are re-evaluated on each loop if applicable.

To change the value of a variable once it's been created, use "@edit_var" a special operator with three arguments. The second argument is the index of the variable to edit, the third argument is the value to set that variable to. The first argument becomes the result, so that "@edit_var" just edits the variable without impacting the running value. For example, [1, "@edit_var", 2, 3, ...] sets the variable at index 2 to the value 3, then the 1 continues as the running value.

There are other similar special operators associated with variables:

Normally, the variables array is local to that specific CalcArray, so children or parents of that CalcArray will not have access to that CalcArray's variables. This is often undesired, because often when using "@edit_var" you want to have the variable's new value be based on its current value, which means you need to access the variable's current value inside a child CalcArray. To allow for this, put "@var_retain" at the beginning of a CalcArray (before the list of variables if it has one), which causes that CalcArray to inherit the variables array from its parent (it'll be the same object, so changes to the variables array made in the child will also affect the parent's variables array). For example, [..., "@edit_var", 1, ["@var_retain", "@Var 1", "*", 2], ...] will change the value of the variable at index 1 to double its current value; if the "@var_retain" wasn't there, the child CalcArray wouldn't retain the variables of its parent, so "@Var 1" wouldn't find anything since that child CalcArray would have no variables.

You could instead use "@var_copy", which does something similar but makes a copy of the variables array instead of transferring it outright (so changes the child makes to the variables array won't transfer back to the parent), but I find that usually "@var_retain" is what you want.

If your CalcArray has a lot of nested layers, putting in a bunch of "@var_retain"s can get annoying quickly, so there's a shortcut: if you put "@global_var_retain" at the beginning of a CalcArray, then not only will it retain the variables from its parent, the variable retaining will automatically cascade to all of its children, and all of its childrens' children, and so on. Likewise, there's "@global_var_copy", and there's also "@global_var_none", which stops a global_var cascade coming from its ancestors from applying to that CalcArray or its children.

Finally, there's also the "game variables"; whereas most variable arrays are local to a specific CalcArray, the game variables are a single array that exists across the whole mode (and is typically initialized before the game starts by the code to set up the mode being played) and can be accessed by any CalcArray. Use "@GVar 0", "@GVar 1", "@GVar -1", "@GVar -2" and the like to access their values, and use "@edit_gvar", "@add_gvar", "@insert_gvar", and "@remove_gvar" to alter them. Since game variables are global, there's no need for a "@var_retain" equivalent, but there sort of is one anyway: if you put "@include_gvars" at the beginning of a CalcArray, then the current values of the game variables will be copied into the beginning of the variables list in that CalcArray. "@include_gvars" was added before "@GVar" strings, so it's an outdated feature you probably shouldn't use (just use "@GVar" strings to access them), but I still had to mention it.

Array Operators with CalcArray Arguments

Remember those seven array operators I mentioned earlier as being too complicated to discuss yet? That was because using them requires an understanding of variables, so now I can tell you how they work. Each of these operators has one of its arguments be a CalcArray expression; what that expression does depends on the operator, but in all of these cases, it will be run multiple times. These expressions themselves will have "inputs" that come from the array being operated on (these inputs change on the different runs of the expression), and the way this is accomplished is by adding those inputs as variables at the end of the variables array of that CalcArray expression, so within the expression you use "@Var -1", "@Var -2", etc. to access their values.

Here are the seven operators in question:

And now for perhaps the most complicated operator of all: "CustomDIVESeedUnlock", a very complicated version of the DIVESeedUnlock operators that lets you customize how it works, allowing you to use the DIVE seed unlocking algorithm on things that aren't just BigInts or GaussianBigInts, with your own definitions as what counts for things like division. This operator takes a whopping eleven arguments. The first three arguments do the same thing as they do in the other two DIVE seed unlock operators, while the rest of them all represent functions, and thus have you use "@Var -1" and sometimes "@Var -2" to represent the argument(s) to those functions. Here's what the rest of the arguments do:

"CustomDIVESeedUnlock" is used in a couple waves modes with wacky seed unlock behaviors, such as Dual-Tile DIVE (which needs "CustomDIVESeedUnlock" so it can do its multiplications, divisions, etc. in Z2).

Part 4: How CalcArrays are Used in Modes

CalcArray()'s Other Arguments

So far, everything we've discussed has been within the CalcArray, i.e. the primary argument (argument #0, since the JS arguments array is 0-indexed) to the JS CalcArray() function. But that's not the only argument CalcArray() can take (though it is the only required one)!

Arguments #1 and #2 to CalcArray() make it so it's called "on a specific tile": argument #1 is the vertical coordinate of that tile, argument #2 is the horizontal coordinate of that tile. In the Power Compendium's grid, increasing the vertical coordinate moves downwards, increasing the horizontal coordinate moves rightwards. These are both considered 0 by default.

Arguments #3 and #4 establish the direction of movement. Argument #3 is the vertical component of the movement direction, argument #4 is the horizontal component of the movement direction. These are both considered 0 by default.

Argument #5 is an array of additional arguments (it was made to be an array so that if I add any more info later on, I won't have to mess with the order of the arguments). As of now, this array has meanings for up to four arguments: index 0 has the length of the current merge if one is occuring, index 1 has the maximum spaces per move of the current movement direction, index 2 has the "move type" (I'll explain what "move type" is in the Movement Directions section later), and index 3 has what the index of the current movement direction is (a positive integer for manual directions (so this would be 3 for the entry of the directions array with index 2), a negative integer for automatic directions, 0 for no direction). This argument is [1, Infinity, 0, 0] by default.

Argument #6 is the grid of tiles that is being worked on. The default here is the normal grid; when something else is being used for this argument, it's usually something like the array of next spawning tiles.

Arguments beyond that probably shouldn't be messed with even if you're writing code for the Compendium, unless you're writing a function that's related to the running of CalcArrays themselves or something along those lines, as they're data CalcArrays pass between themselves for recursion purposes and the like. But, for completeness's sake, here's what they do anyway: argument #7 is the array of parent values, argument #8 is the variables array, argument #9 is the "global variable stat" ("@global_var_retain" sets this to 1, "@global_var_copy" sets this to -1, "@global_var_none" sets this to 0), argument #10 is an argument called "inner" (that's usually true) which determines whether this actually counts as a child CalcArray (thus adding its running value to the parents chain) or not (if, else, else-if, and repeat do a recursion call but without actually counting as a child CalcArray), and argument #11 is an argument called "outermost" that stores whether this array is the outermost layer of the CalcArray expression (the CalcArrays inside operators like "arr_sort" that are acting as separate functions also have this set to true).

If you're making a 2048 Power Compendium mode, you'll be writing most of your "code" in CalcArrays themselves, so you shouldn't be worrying about calling the CalcArray() function yourself - that's usually left to the "engine", although sometimes certain modifiers (like random goals) do have to deal with this. However, understanding what data a CalcArray tracks will be useful for the rest of this part.

Internal Representations of Tiles

Before I can explain how things like tile display rules and merge rules work, I have to explain what a tile actually is internally. I believe most 2048 variants create a Tile class for this kind of thing, but my mentality is usually "don't make a new class unless you have to". Tiles don't really have a need for methods and such - all that's important to a given tile is its value(s) and its position. As such, in the 2048 Power Compendium, tiles are just arrays, usually of numbers. For example, in 2187, a tile is a two-element array, where the first number is the power of 3 it is and the second number is what that power of 3 is multiplied by. For example of example, 162 is 34 times 2, so in 2187 the 162 tile internally is [4, 2]. Most Page 1 modes follow this pattern (with the base of the power part changed, of course).

Different modes represent tiles in different ways internally. Here are some examples:

Of course, I didn't give every notable example here - there are some other interesting ways tiles are represented, so if you're interested in examining the Compendium's code, you might want to go through some modes and see how they store their tiles.

These first two sections of Part 4 haven't really been about CalcArrays, have they? I included them here because they provide context that's needed for what comes next.

Other Special Strings

@Parent, @Var, and @GVar strings aren't the only special strings that CalcArrays can refer to. Most of the rest of them refer to in-game objects, which is why I've been putting them off until now. Here's a list of them:

Color Expressions

A color expression is an array similar to a CalcArray that represents some color. Instead of the CalcArray() function, these are evaluated via evaluateColor(); the first argument of that function is the color expression, the next two are the vertical and horizontal coordinate, but then the next one is the grid/tile container being used; color expressions don't support detection of movement direction, next tiles, and so on. A plain hex string, like "#ff0000", is a valid color expression, but if you want the color expression to vary based on the current tile or somesuch, you'll need to use one of the arrays. evaluateColor() will convert the color expression into a string that can be used as that color, or gradient, in the HTML/CSS.

The most common color expressions are those that represent single colors. These are five-element arrays: the 0th element is a string saying which color system it's in, the following four are its dimensions in that system. The most common 0th element is "@HSLA", for which the 1st element is the hue (0 is red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 is magenta, 360 is red again), 2nd element is the saturation (100 is fully saturated, 0 is greyscale), 3rd element is the lightness (100 is white, 0 is black, 50 is the non-tinted color), and 4th element is "alpha"/opaqueness (1 is fully opaque, 0 is invisible transparent). For example, ["@HSLA", 200, 90, 60, 1] would be this color. The other two are "@HSVA" (1st element is hue, 2nd is saturation (100 is pure color, 0 is greyscale), 3rd is value (100 is fully light, 0 is black), 4th is alpha) and "@RGBA" (1st element is red (0 to 255), 2nd is green (0 to 255), 3rd is blue (0 to 255), 4th is alpha (still 0 to 1)). The four latter elements need to result in numbers, meaning they have to be either plain numbers or CalcArray expressions that result in numbers.

Next are the gradient types. A color expression beginning with "@linear-gradient" will result in a linear gradient of colors. Each entry after that should be either a color expression that's a single color, or a number (which places the most recent color at that percent through the gradient). If there's a number right after the "@linear-gradient" (i.e. before any color entries), it sets the angle of the gradient (0 is bottom-to-top, 90 is left-to-right, 180 is top-to-bottom, 270 is right-to-left, and values between multiples of 90 will be some form of diagonal) For example, if you want a gradient that goes from left-to-right, starts at red, goes to yellow 20% of the way through and stays yellow until 45% of the way through, then ends at blue, you'd do ["@linear-gradient", 90, ["@HSLA", 0, 100, 50, 0], 0, ["@HSLA", 60, 100, 50, 0], 20, 45, ["@HSLA", 240, 100, 50, 0], 100], or some variation of such. (Note that any CalcArray within a gradient will be assumed to be a color, even if it would result in a number. To mark a CalcArray within a gradient as "this will result in a number for a gradient position, not a color", put "@CalcArrayNumber" at the start of that gradient entry.)

The other gradient types are "@radial-gradient" (gradient positions are still from 0 to 100, with 0 being the center and 100 being the edge), "@conic-gradient" (gradient positions are from 0 to 360, 0 is at the top and it goes clockwise from there), and "@repeating-linear-gradient", "@repeating-radial-gradient", and "@repeating-conic-gradient" are versions of the previous three where, if the last color isn't at the end of the gradient, instead of just having the last color last until the end, it jumps back to the first color and repeats the cycle.

"@multi-gradient" results in multiple gradients stacked on top of each other. Each entry after the 0th in a "@multi-gradient" array should itself be a gradient color expression.

"@rotate" has three elements after the starting string, and what it does is take another color expression and rotate its hue by some amount around the color wheel (clockwise, so 90 degrees would rotate reds to chartreuses, yellows to sea greens, blues to rose magentas, etc.). The 1st element is the amount of degrees to rotate by, the 2nd element is a boolean that, if true, also inverts the lightness of the color (lightness becomes 100 - lightness), and the 3rd argument is the color expression to be rotated. If the color expression is a gradient or multi-gradient, all of the colors in it are rotated.

If the first entry of a color expression is "@CalcArray", then the array with that first entry removed is first evaluated as a CalcArray, and then the result of that CalcArray is what's evaluated as a color expression.

Text Expressions

Before v3.0, displayed text could freely include HTML elements like <b> and <sup> - Isotopic 256 used this for its atomic numbers, for one example. But it was eventually pointed out that this allowed for a "scope escape", allowing CalcArrays to in theory run HTML, CSS, and maybe even JS code outside of its purview. As such, v3.0 closed the scope escape by running these text strings through HTML entity encoding (using the open-source library he), but this would have disallowed superscripts, Unicode characters beyond ASCII, and other such things entirely. As such, the "text expression" system was introduced.
Under many circumstances, a text expression is just evaluated as a CalcArray, except its result is always converted into a string and encoded by he. However, there are special strings that, if the array starts with one of those strings, the array is in "text expression mode" instead, where each entry is itself evaluated as a text expression, and then all those strings are concatenated together. There can be multiple of these strings in front of the same text expression. The he encoding only occurs on the CalcArray layers, so outside of those, some of these HTML things are supported - but calculations can't be done outside the CalcArray layers, only string concatenations and certain supported text effects. For example, the text expression ["@TextE", "Hi! ", ["@Bold", "woah, "], ["@Italic", ["a", "str_concat", "b"]]] would result in "Hi! woah, ab", as the ["a", "+", "b"] doesn't start with one of the text expression strings, so it's treated as a CalcArray. Once you're inside a CalcArray layer, you can't put text expression layers inside it, the text expression stuff needs to go on the outside.

Text expressions are used in places like the text on tiles, the rules text, and the text on stat boxes. Here's the list of starting strings that trigger text expression mode:

The starting string "@ComposeTE" also has effects involving text expressions, but you shouldn't use this. What it does is evaluate the rest of the array as a CalcArray, then take that CalcArray and turn formerly supported HTML elements like <sup> into their text expression counterparts. This was designed for use in the code to import pre-v3.0 save codes, not for use by modders.

Tile Display Rules

We've covered most of what CalcArrays do themselves now, so it's time to discuss the places in the 2048 Power Compendium mode they're contained within. I'll use 2187 as my primary example, though I'll pull from other modes where it's necessary.

First of all, how are tile displays generated? Here's what the TileTypes array looks like in 2187:

Each entry of TileTypes defines one tile display rule. The 0th entry of a display rule is either an array like [1, 2] that would match a tile, or a CalcArray expression that results in a a boolean. When a tile is looking for what display rule to use, it goes through TileTypes from start to end, stopping once it hits a rule where either the tile array is the same as the 0th entry of the display rule, or running the 0th entry of the display rule as a CalcArray on that tile results in true (if the array is of the "match a tile" type, it may still try to run it as a CalcArray, but since an invalid CalcArray operator just results in the first argument, doing so will end up resulting in a number, and thus not the boolean value true). In the event every display rule fails, it defaults to the last one.

Once a display rule has been decided, the rest of that rule's entries control the tile's display. The 1st entry is the number that'll be displayed on that tile (this can actually be any type, or a CalcArray that results in any type. If it's a numeric type, that type's defaultAbbrev operator will be run on it after the value is calculated), the 2nd entry is the color or gradient of the tile's background (a color expression), and the 3rd entry is the color of the tile's text (a color expression). Tile backgrounds are allowed to be single colors or gradients or multi-gradients, but gradient text is not supported, so tile colors must be single colors.

Some tile types will have additional entries, as seen in the Ratio-Fill modes:

The 4th entry controls the text's shadow effect. This can be written the way CSS does it directly, or as an array of four elements: in the latter case, the first two elements (numbers) control the horizontal and vertical offset of the shadow from the text, the third element (a number) controls the blur strength, and the fourth element (a color) is the color of the shadow. Of course, any of those elements could be CalcArrays (in the case of the fourth it'd be a color expression instead).

Under normal circumstances, a tile's text size is based on how many characters are in the text - for the most part. The "text length" of a tile is treated as either 2 or (the amount of characters * 0.7), whichever is higher, and then larger text lengths mean smaller text sizes (inversely proportional). The 5th and 6th entries of a display rule let you alter this: use a positive number to mean that exact number, use a negative number to mean (the amount of characters * abs(that number)), and then the text length will be treated as whichever of those two is higher. (If one of them is set to 0, it reverts to its default, which is 2 for the 5th entry and -0.7 for the 6th)

Finally, any entries beyond the 6th are addons. There are currently three possible types of addons:

Special Color Schemes

Most modes use color expressions to determine the colors/backgrounds of their tiles, but there's a collection of color schemes that were too complicated to implement via CalcArrays, and thus had to be directly implemented in JavaScript instead. These are the "special color schemes". To use a special color scheme for a tile instead of its normal display, have "@ColorScheme" be the display rule's 2nd entry, then put the name of the special color scheme for the 3rd entry, and the 4th entry should be an array whose 0th entry is the value (usually a BigInt) to input into the special color scheme, and further entries are "parameters" for that special color scheme. Here's an example of what this looks like:

And here's a list of the special color schemes:

Special color schemes can also be used in PrimeImages. To do so, instead of having the 1st entry of the prime image be the gradient, have it be an array of the form ["@ColorScheme", whatever the special color scheme is, the value to put into the color scheme, ...color scheme parameters]. (PrimeImages can also have a tile display rule used for them, by having the 1st entry of the prime image be the tile display rule with "@DisplayTile" replacing the boolean check of the tile display rule)

Merge Rules

TileTypes is one of the two main places where CalcArrays are used in every mode. The other one is MergeRules, which is in my opinion the most important part of a mode, since it's where the rules of the mode, i.e. what tiles can merge, are. Here's 2187 again:

Like with TileTypes, entries earlier in MergeRules are checked first when tiles collide. But an additional precaution has to be taken here: if, for example, two or three of the same tile can merge, putting the three-tile merge first is necessary, but not sufficient, because the two-tile collision will occur before the three-tile collision does. This is why @NextNE strings exist, as seen in 1296's MergeRules:

["@NextNE -1 0", "!=", "@This 0"] in the third merge rule is there specifically to disallow the two-tile merge if the three-tile merge is coming up.

Modes like Isotopic 256 have effects that occur to individual tiles at the end of a turn. In many cases, this is done by a merge whose length (0th entry) is 0. Length-0 merges do not occur during the moving process of a move; instead, they occur once all the tiles have finished moving and merging (but before new random tile(s) spawn), and they occur to each tile individually. Merge length 0 is the official way to do "merges" that only alter one tile. Merges with a length of 1 are not officially supported, and I'm not sure what the engine would do if you tried to include one. I suspect it'd be like a length 0 merge but it can occur in the middle of a move instead of only at the end, but I don't know.

A few merge rules, like those in XXXX, have ten entries instead of six:

When this is the case, that merge rule can take on multiple lengths. This works by having it start at the given length, then create copies of the rule by incrementing the length, and at each increment it pastes in a new copy of the 1st entry's CalcArray (the 1st entry now becomes a CalcArray containing all of the copies, separated by "&&"s), but with some of the "@Next" strings having their first index increased on each copy (in this context an "@This" string acts as an "@Next 0" string and thus also increments). The 0th entry now instead refers to the minimum merge length that the rule is valid for, the 6th entry is the length that the merge rule as given is, the 7th entry is a list of how much to increment the first index of the "@Next" strings by (Strings that were "@This" in the original are incremented by the 0th entry of the 7th entry, strings that were "@Next 1" in the original are incremented by the 1st entry of the 7th entry, strings that were "@Next 2" in the original are incremented by the 2nd entry of the 7th entry, and so on), the 8th entry is how much to increase the merge length on with each increment, and the 9th entry is either a single number (the maximum allowed merge length) or an array of numbers (only those merge lengths are allowed). A merge rule has to have five, six, or ten entries; the 5th entry isn't necessary on its own, but once you're going for a multi-length merge rule you need to have all four of the multi-length entries. The turning of a multi-length merge rule into multiple copies occurs at the start of the game, so the multi-length merge rule will not remain as a single merge rule once the game has begun.

It is possible for a merge to have more outputs than inputs. This is called a "Merge Overflow" merge, and in this case there's a few special strings you can use to indicate how the overflow behaves. These strings go at the start of the 3rd entry of a merge rule, i.e. at the beginning of the array of output tiles. Here are the merge overflow strings:

Tile Spawns

The variable startTileSpawns is used to define the possible spawning tiles. Each element of this array is itself a two-element array representing one possible spawning tile, where its first element is the tile itself and its second element is the chance of it spawning. These chances do not have to be out of 100%; it simply means that a tile with a larger chance is more likely to spawn than one with a smaller chance. Normally the spawning tiles and chances are written as plain arrays and numbers respectively, though you can put CalcArrays into a tile's entries or a spawning chance if you wish. If you want an entire spawned tile to be evaluated as a CalcArray expression (which you'll need to do if you don't want the result to be an array, such as if you want to spawn a temporary hole), put "@STCalcArray" at the start of that spawning tile.

If a spawning tile array's first entry is "Box", then the second entry is still the chance, but there are more entries after that. This is used to denote spawns like those in 3072 or 1535 1536 1537, where there's a "box" containing a select amount of select tiles that refills when it runs through them all, ensuring a particular long-term distribution of spawning tiles instead of leaving it up to random chance. After the second entry, the next entry is a spawning tile, then the next entry is how many of that tile are in the box, and repeat.

There's also the start_forcedSpawns array. Each entry of start_forcedSpawns is an array consisting of a CalcArray expression, a string, a boolean or an array of one or two booleans, and one or more tile arrays. If the CalcArray expression is true this turn, then all of the tiles in this entry will be spawned this turn, either before or after the random spawns depending on the string, which can be "BeforeSpawns" or "AfterSpawns". This is used by the Temporary Holes modifiers. If the boolean, or the 0th entry of the boolean array, is true, then if that tile fails to spawn because the board is full, you immediately lose. If it's a boolean array and it has a 1st entry that's true, then that forced spawn will only occur once, i.e. once it spawns (or fails to spawn because the board is full) it's removed from the forced spawns array.

startTileSpawns and start_forcedSpawns are what the tile spawns and forced spawns are at the start of a mode. There are CalcArray operators (more on those later) that can edit the tile spawns and forced spawns, so there are also variables TileSpawns and forcedSpawns that store the current spawns instead of the starting ones (but if your goal is to make an injected mode, you should only be working with the starting versions, not the current ones).

Scripts

Some modes have "scripts", CalcArrays that aren't associated with any other object, and are instead evaluated on their own at particular times in the game. These are primarily used for random goals, but some modes have other uses for them. For example, here's what 1762's scripts entry looks like when its random goals are in the "Random goals build upon the previous goals." setting:

Each entry of scripts is an array with two elements, which collectively are a "script": the 0th element is the CalcArray to be evaluated, and the 1st element is a string that indicates when this script should be triggered. Though the script's CalcArray has to be a full CalcArray, meaning it has to start with a running value and eventually result in something, the result value of a script isn't used for anything, so scripts are only useful if they modify things outside themselves. Most of the time, this means modifying the game variables. In the 1762 random goals example, "@GVar 0" is being used to store the current random goal, "@GVar 1" is storing how many random goals have been reached so far, and "@GVar 2" is a boolean that's normally false. The first script's job is to set "@GVar 2" to true when a tile equal to the current goal is merged, and the second script's job is to increase the random goal at the end of the turn, including increasing your goals reached count, setting "@GVar 2" back to false, and doubling the random goal then increasing it by -1, 0, or 1 at random (since the tiles reachable from a tile N in 1762 in one merge are 2N-1, 2N, and 2N+1).

The process of evaluating a script is, for the most part, no different than evaluating any other CalcArray. The new part here is that string indicating when to run the script. Here's the list of such strings:

The 1st element can be an array of strings instead of a single string, in which case the script triggers whenever any of those strings occurs.

Interval Scripts

There's one more script type I haven't mentioned: "Interval" scripts, which are complicated enough to get their own short section.

There's a variable in the code named displayGridIntervalTime, which is Infinity by default but can be set to a finite number. If displayGridIntervalTime is finite, then every that many milliseconds, even if a move is not currently in progress, the display of the grid, stat boxes, background, etc. is updated - this is useful for modes where "@TimeSinceStartGame" and the other such real-time variables are used in some way.

Whenever this grid re-display occurs, Interval-type scripts are checked. Whereas other scripts only have two array entries (the CalcArray to run, and the script type), Interval scripts have a third argument, which must be a number. When the Interval scripts are checked, each Interval script will only run if it has been at least that many milliseconds since the last time that script ran. Furthermore, an Interval script can also have a fourth argument (another CalcArray); each time the Interval script would be run, if that CalcArray results in true, then that Interval script stops running for the rest of the game.

Stat Boxes

The boxes above the grid that keep track of statistics in the game, such as your score or the Discovered Tiles, are also defined via arrays containing CalcArrays and other entries. Each entry of the statBoxes array is a single stat box, and for many modes statBoxes is just [["Score", "@Score"]], meaning the only stat box is the score box. (wondering where it is in the mode definitions for some modes? It's at the top of loadMode(), set before the individual mode defining code, so if a mode doesn't do anything else with the statBoxes then it defaults to that, rather than the default being specified in every mode it applies to). But some modes have more going on with their stat boxes. Here's 180's:

And here's DIVE's:

When a mode has multiple stat boxes, the ones listed first in statBoxes are the leftmost ones. For those unfamiliar with JavaScript syntax, the arrays of commas with ... before them are used to mean that an amount of arguments equal to the amount of commas are set to their defaults. So that means there's a lot of possible elements for stat box definitions! Only the 0th and 1st elements are required, though. Here's what they all do:

With all those entries put together, it's theoretically possible to make a stat box behave as a button that does something when pushed, though no Compendium mode has done this yet - clickable stat boxes were added mostly for the sake of toggling how Discovered Tiles is displayed.

Movement Directions

You probably won't interact with these much when making a modded mode, since the directions can be fully edited using the modifiers (so there's no real need to mod them), but in case you do need to mod these for some reason, here's how the movement directions are stored internally. First, here's what it looks like by default:

The 0th entry of directions[x] controls the actual behavior of that direction: directions[x][0][0] is the vertical movement magnitude, directions[x][0][1] is the horizontal movement magnitude, directions[x][0][2] is the maximum spaces per move, and directions[x][0][3] is the "move type", which is 0 for manual moves and -1 for automatic moves. directions[x][0][4] is always [1, true, true, true] for manual directions.

The rest of the entries are mostly about the display of the button: The 1st entry of each direction is the text on the arrow button, the 2nd entry of each direction is the size of the button, the 3rd entry of each direction is the font size, the 4th entry of each direction is the vertical position, the 5th entry of each direction is the horizontal position, the 6th entry of each direction is the keyboard keys associated with that direction, and the 7th entry of each direction is the rotation of the text on the button.

Now for the automatic moves. Here's what one of those looks like by default:

The 0th entry of an automatic move is the same as the 0th entry of a manual direction: it's the array that controls the actual behavior of the automatic move. The [x][0][4] array discussed earlier is the "manual strength" array, and here's what it does: the 0th manual strength entry is 1 for manual moves, 0 for automatic moves that count as their own separate move, and -1 for automatic moves that count as part of the manual move that they trigger during. The 1st manual strength entry is whether regular merges can occur during the automatic move, the 2nd manual strength entry is whether length 0 merges can occur during the automatic move, and the 3rd manual strength entry is whether scripts that execute at the beginning or end of a move (BeginTurn, EndMovement, EndTurn, etc.) execute on this automatic move. In the Modifiers, "This automatic move counts as its own separate move." sets the 0th, 2nd, and 3rd manual strength entries to 0, true, and true, while "This automatic move counts as part of the manual move that it triggers during." sets the 0th, 2nd, and 3rd manual strength entries to -1, false, and false; "Tiles can(not) be merged during this move." only controls the 1st manual strength entry. An automatic move can also have an [x][0][5] entry, which is the percent chance that that automatic move happens.

The 1st entry of an automatic move is when the automatic move occurs, which is a string, either "Between", "Before", or "After". The 2nd entry of an automatic move is a CalcArray; if this entry is present, then the automatic move only triggers if that CalcArray results in true.

Other Special Operators

There are some CalcArray operators I didn't mention in the previous parts because they access or modify some of the in-game objects that were introduced here in Part 4, so here's a list of them:

Part 5: Infused and Injected Modes

Ways to Create a Mode

We've covered basically everything relevant to CalcArrays at this point, but knowing how to use CalcArrays doesn't mean much if you don't know how to put it all together into your own modes.

There are two types of modded mode that you can make for the Compendium. The old way is the "injected modes" that have been mentioned previously; these are made either by by editing the Power Compendium's code directly to make the mode and then exporting it as a save code, or editing an existing save code into a new mode. Injected modes are saved as @2048PowCompMode save codes, which take you directly into the game when imported. Since they're shared by using the save code functionality designed for saving in-progress games, injected modes won't respond to the global Modifiers, nor can they have mode modifiers of their own, as all of those details are already in the save code.

v3.0 introduced a second, more official way to create modded modes, known as Infused Modes. Type "Infuse" (without the quotes) into any save code entering location in the Compendium and import that, and it will take you to an interface where you can create these Infused Modes, as well as put them onto additional pages of the title screen grid. The Infused Mode Guide in that area explains the grid part, so I'll focus on the creation process here.

Infused Mode values

Creating an Infused Mode is done by editing the 40+ variables on the "Create a New Infused Mode" screen. The strings you enter here will be imported into the appropriate variables when the mode is played. These will run through the same JSON interpreter that save codes do - so you need to use the "@BigInt n", "@BigRational n d", and "@GaussianBigInt r i" syntax for values of those types, as well as "@Infinity", "@-Infinity", and "@NaN" for those respective floating point values, and "@undefined" for undefined (since those last four values would all turn into null in normal JSON). Reading this section will be useful for injected modes too, as many of these same values are used in those.

Here's what each of the values here does, and the type it needs to be:

  1. TileNumAmount (number): The amount of elements the tiles have in their arrays. This must be a constant number - every tile in a given mode must have the same amount of elements. If you want a tile to have a variable amount of elements, make one of its elements an array itself and put the extras there (3069 does this).
  2. TileTypes (array of tile display rules): The array of tile display rules that controls the displays of tiles. See the Tile Display Rules section for details.
  3. MergeRules (array of merge rules): The array of merge rules that control what merges can occur. See the Merge Rules section for details.
  4. startTileSpawns (array of tile spawn arrays): The array of (random) spawning tiles. See the Tile Spawns section for details.
  5. start_forcedSpawns (array of forced spawn arrays): The array of forced tile spawns. See the Tile Spawns section for details.
  6. statBoxes (array of stat box arrays): The array of statistic boxes, like the score box. See the Stat Boxes section for details.
  7. start_game_vars (array): The array of game variables at the start of the game. See the Variables section for details. (P. S. The reason TileSpawns, game_vars, and forcedSpawns have "start" versions is because of the CalcArray modifiers that can modify them, meaning they need to be reset to their starting versions when the game is restarted. Infused modes have you edit the starting versions, not the in-game versions, and you should do the same when making an injected mode.)
  8. scripts (array of scripts): The array of scripts. See the Scripts section for details.
  9. tileDisplayKnownLevel (0, 1, 2, or 3): v2.1.13 added a system where the game keeps track of what tiles that have already been seen look like, to avoid having to run CalcArrays every time you want to display a known tile, as CalcArrays are much laggier than just running JavaScript where possible. The strength of this system is controlled by tileDisplayKnownLevel: 0 means that this memory system is not used at all, 1 means that this memory system is only used on a per-turn basis (i.e. its learned displays are forgotten at the end of each turn), 2 means that this memory system retains displays of tiles currently on the board but gets rid of the ones that aren't on the board anymore, and 3 means that this memory system retains all tile displays from throughout the game. Note that this is a maximum: since there's a Setting that limits this, and Infused Modes respond to Modifiers (which includes Settings), the actual tileDisplayKnownLevel can be put to below what the infused mode says it is by that Setting, but it can't go above this value. There usually isn't much reason to not make this 3, but in theory you could make a mode where the displays of tiles are dependent on something other than their own elements, so if you do you'll need to lower this.
  10. mergeResultKnownLevel (0, 1, 2, or 3): Similar to tileDisplayKnownLevel, but for storing the results of merge rules (including storing what collisions failed to merge). Level 2 retains merges where all of the tiles are still on the board. Modes such as 2592 and 2295 have to lower this since their merges change rules over time. This system does work with Partial Absorb merges, but it does not work with Merge Overflow merges (merges with more outputs than inputs), so you'll need to set this to 0 if your mode has Merge Overflow merges.
  11. knownMergeMaxLength (number): The maximum merge length of the mode. You don't need to set this if mergeResultKnownLevel is 0, but if you do intend to use known merge results, they need to know how long the merges can get (but you can set knownMergeMaxLength to Infinity if you need to). If you're making an injected mode, remember to set this - it's easy to forget this one when making an injected mode! (Infused modes set it to Infinity by default, so you'll just be losing some optimizations if you forget it there, but with injected modes forgetting this is a common cause of bugs)
  12. knownMergeLookbackDistance (number): How far back into the negative @NextNE tiles the known merge results will examine. If this is above 1, it also checks @Next tiles in front of the merging tiles (one less of those than it does of the ones behind).
  13. winConditions (array): This array defines the tiles that will be added to the Discovered Winning Tiles when discovered. Each element of this array is either a tile array (so only that tile counts as a winning tile) or a CalcArray expression (any tile that makes that expression evaluate to the boolean true will be considered a winning tile), similar to how the tile for Tile Display Rules can be either of those.
  14. winRequirement (number, false, or CalcArray): The amount of discovered winning tiles required to win the mode. Can also be set to the boolean false, in which case the mode cannot be won. Can also be set to a CalcArray, in which case you win if that CalcArray results in true (in this case winConditions is ignored... unless the win requirement includes "@DiscWinning", of course)
  15. loseConditions (array): Same as winConditions, but for premature loss instead of victory.
  16. loseRequirement (number, false, or CalcArray): Same as winRequirement, but for premature loss instead of victory.
  17. winPriority (boolean): True by default. If this is true, then if you get into a situation where you win and lose on the same turn, you win. If this is false, then you lose in that situation.
  18. postgameAllowed (boolean): True by default. If this is false, then you cannot continue the game after you win.
  19. gameplayBackground (color expression): The background color or gradient during the game. Before v3.0 this and other backgrounds would only be evaluated at the start of the game (and were stored directly in CSS instead of as JavaScript variables), but now the backgrounds can be updated during the game.
  20. gridBackground (color expression): The color or gradient of the back of the grid (the spaces between the tiles).
  21. tileBackground (color expression): The color or gradient of the empty tiles. If the first entry is "@DisplayTile", then the array with that entry removed is treated as a tile display rule to use on the empty tiles.
  22. textBackground (color expression that results in a single color): The color of the rules text and such. Unlike the other backgrounds, this cannot be a gradient.
  23. gameplayRulesText: A list of HTML elements that comprise the rules text during the game. Each entry of this array is itself a two-entry array, where the first entry must be "p", "h1", "h2", "h3", "h4", "h5", or "h6", and the second entry is a text expression. For an infused mode, you may instead set this to the boolean true to have it default to being the same as gmRulesText; you cannot do this for an injected mode.
  24. Default grid size (number): Decides what the default grid size is for this mode. For example, if this is 5, the default grid size for a square grid for this mode is 5x5.
  25. movementParameters (array of three CalcArray expressions): This is ["@VDir", "@HDir", "@SlideAmount"] by default. When a tile is moving, it's actually these, not the directions and maximum slide amount themselves, it looks at to determine how it moves, which means if you change these expressions, you can make it so different tiles move in different ways when you move in the same direction.
  26. spawnConditions (CalcArray): If this CalcArray results in false when run at the time tiles are supposed to spawn, then tile spawns will not occur that turn.
  27. tileValueFunction (CalcArray): An expression that should result in the "value" of a tile. You technically don't need to bother with this unless you intend on using the tileValue operator, as right now nothing in the Compendium relies on this - but someday there might be a modifier that uses this...
  28. discoveredTilesFilter (CalcArray): A CalcArray (missing its first entry) to be run on each potential new discovered tile before adding it to the discovered tiles list (if it's still undiscovered after doing so). This is used to make it so Discovered Tiles doesn't store duplicates of tiles that are, depending on the mode and modifiers in question, considered "the same tile" despite their tile arrays being technically different.
  29. infusedModeModifiers: The array of mode modifiers for this infused mode. See the Infused Mode Modifiers section (the section after this one) for details.
  30. gmBackground (color expression): The color or gradient of the pre-game screen (the screen that appears when you first enter the mode, where the Start Game button and the mode modifiers are).
  31. gmRulesText: A list of HTML elements that comprise the rules text on the pre-game screen. Uses the same format as gameplayRulesText.
  32. Big tile text (string): The text written on the icon tile for this mode.
  33. Big tile font size (number): The font size of the text on the icon tile. For reference for scale, 4.8 is the font size used for four-digit mode titles like 2048.
  34. Big tile background (color expression): The color or gradient of the icon tile.
  35. The options in the "Respected/Ignored" box are for turning off the usual effects of particular modifiers that are likely to not mesh well with certain modes. The modifiers included here, especially Negative Tiles, are generally ones that I felt some modes are likely to want to support but not just using the default method of implementing it, so they'd be better off Ignoring it and then implementing their own behavior when it's on, since if it's not Ignored it will use the default code for adding it in. (I'll explain how to detect modifiers as such shortly)
  36. gmDisplayScript (CalcArray): This CalcArray is run every time any setting on the pre-game screen is changed. Useful for limiting or deciding the values of certain mode modifiers based on other mode modifiers.
  37. displayGridIntervalTime (number): The grid, statBoxes, etc. are re-displayed once every this many milliseconds even if you're not moving, so that values related to real time can be displayed over time rather than having to wait for the next move to update visually. Interval scripts are only checked on each run of this display grid interval.
  38. modifiersOverride: An array of forced values for particular Modifiers. Each entry of this array is itself a three-entry array, where the first entry is the (number) index of the modifier to be edited (more about modifier indices in the Modifiers section later), the second entry is the value to set the modifier to, and the third entry is a boolean (if false then the modifier is changed immediately when the mode is loaded, if true then the modifier is only changed as the game begins). modifiersOverride does not work on modifiers[5] through modifiers[11], the modifiers related to the grid size/grid shape/custom grid.
  39. settingModifiersOverride: Same as modifiersOverride, but for the Settings instead. Doesn't do anything to the Settings related to saving things on reload.
  40. spawnTileBlocked (CalcArray): If this CalcArray results in true at a particular spot on the grid when a tile is trying to spawn, that spot will not be a valid spawning location for that tile. If no empty spot is valid, then the tile fails to spawn. (Note that this does not override the "New tiles can only spawn on the edge that you just moved away from." Modifier; if that Modifier is on, only spots on that edge will make it far enough to even be tested by spawnTileBlocked).
  41. directionBlocked (CalcArray): If this CalcArray results in true on a particular direction, you cannot move in that direction this turn. "@VDir", "@HDir", etc. in this CalcArray use the appropriate values for that direction.
  42. gameplayComponentOpacities (array of up to nine entries, each of which is a number or a CalcArray that results in a number): Each of these values alters the opacity of some element of the gameplay. 1 is fully opaque, 0 is invisible but still there and still clickable, values between 0 and 1 are somewhat transparent, anything below 0 is hidden entirely and cannot be clicked. Entries after the last one are assumed to be 1.
    The order of the entries is as follows:
    • Opacity of the directions (if this is below 0 then keyboard moves won't work either, you can't move if the direction box is hidden entirely this way).
    • Opacity of the grid (the grid still functions internally even if it's hidden)
    • Opacity of the rules text
    • Opacity of the stat boxes
    • If the array gets this far, then the previous entry is only the opacity of stat boxes on top, this entry is the opacity of stat boxes on bottom
    • Opacity of the "Next Tiles" display (this defaults to the grid opacity if the array doesn't get this far)
    • Opacity of the "Get a save code" button
    • Opacity of the "Restart game" button
    • Opacity of the "Return to menu" button
  43. displayLagReductions (array of up to eight entries, each of which is a boolean or a CalcArray that results in a boolean): Each of these toggles, if that CalcArray results in true, causes some aspect of the game to not be redisplayed when the redisplays are supposed to happen. This is intended for use in reducing lag by avoiding redisplaying things when you know their displays will stay the same. Entries after the last one are assumed to always be false.
    The order of the entries is as follows (each of these describes what happens if that entry results in true):
    • The grid tiles do not update their display
    • The background does not update its display
    • The back of the grid and the color of empty tiles do not update their display
    • The rules text and the text color do not update their display
    • The stat boxes do not update their display
    • The visible next tiles do not update their display
    • The direction arrows do not update their display
    • All of the empty tiles are assumed to be the same color
    If any of these CalcArrays retain variables, then "@Var 0" becomes the "type" of the grid display, which can be one of the following: "MoveStep" (for when the grid is displayed after each step of the move), "MoveEnd" (at the end of a move), "Spawning" (for any grid display that occurs while tiles are spawning), "StatBoxClick" and "TileClick" (from clicking those objects), "Interval" (for grid displays from displayGridIntervalTime), "Resize" (for grid displays from resizing the game window), and "Complete" (start of game, win screen, game over screen, returning to the game from the save code screen, "@DisplayGrid" script signals, and so on. Grid displays of this type ignore all of these lag reductions except the same-color-empty-tiles one, they will always redisplay everything).
  44. Most of these can, instead of their normal type, be a CalcArray with "@InfusedCalcArray" as its first entry, in which case, when the game begins, that CalcArray (with the "@InfusedCalcArray" removed) is evaluated, and its result is used as the value for that variable. If "@var_retain" or something else like that is active in that CalcArray, then there are three variables brought in: "@Var 0" is the array of mode modifiers, "@Var 1" is the array of Modifiers, and "@Var 2" is an array of further arguments: ["@Var 2", "arr_elem", 0] is the array of Settings, ["@Var 2", "arr_elem", 1] is the array of manual directions, and ["@Var 2", "arr_elem", 2] is the array of automatic moves.
    Default grid size and the two modifiersOverrides do not have "@InfusedCalcArray" functionality, but modifiersOverride and settingModifiersOverride entries where the third entry is true (i.e. they trigger as the game begins) can have "@InfusedCalcArray" used in their second entries (the CalcArrays that determine the forced value). gmBackground, the three Big Tile variables, and gmDisplayScript can retain those three variables even without "@InfusedCalcArray" in front (and they won't work if "@InfusedCalcArray" is in front). gmRulesText does support "@InfusedCalcArray"; in this case the CalcArray is re-evaluated whenever something is changed on the pre-game screen.
    When it comes to loading in values and evaluating @InfusedCalcArray's, start_game_vars is evaluated first, then tileValueFunction, then discoveredTilesFilter, then the rest of the variables in order. (Those three come first so that later InfusedCalcArrays can use the game_vars and use the operators associated with the latter two). Before the game starts, game_vars and modifier_vars are considered empty.

Infused Mode Modifiers

One of the biggest things setting infused modes apart from injected modes is that infused modes can have mode modifiers. These are determined by the entries in the infusedModeModifiers array. Each entry is an array, whose first entry is the type of modifier that mode modifier is, and the rest are values and CalcArrays for that type of mode modifier. Any of these entries that are CalcArray scripts can var_retain the following variables: "@Var 0" through "@Var 2" are the same things they are in "@InfusedCalcArray" expressions, and "@Var 3" is the value of that mode modifier specifically.

The types of infused mode modifiers:

Modifiers

To use modifiersOverride and settingsModifiersOverride, you'll need to know how the Modifiers and Settings are stored internally, i.e. what modifier each index corresponds to.

The Modifier indices are:

And the Settings indices are:

While an infused mode can't edit or detect the starting grid, it can edit the directions and automatic moves: use modifiersOverride for this, except instead of a modifier index number, put one of the following strings: "Directions" to override the manual directions, "AutoDirections" to override the automatic moves. However, modifiersOverride entries for directions only work on game start (third entry is true), they can't be used on mode enter (third entry is false).

To explain a little more about "@MVarIndex" strings, the mvar_indices array uses positive numbers for Modifiers, so it uses 1-indexing instead of 0-indexing: for example, the modifier variable associated with the "moving in the same direction repeatedly" modifier, which is modifier #20, will have its corresponding mvar_indices entry be 21. Likewise, Settings use negative numbers, so the modifier variable associated with the Discovered Tiles box (setting #2) will have its corresponding mvar_indices entry by -3. This offset by 1 in each direction is to ensure that the #0's don't overlap in theory (even though in practice neither of them have variables).

Save Codes

The 2048 Power Compendium has a few kinds of save codes, but this blog post is documentation on how to make a mode, so I'll only be going over the main kind of save code, the one that loads either an in-progress game or the start of a mode, as those are what you'd use to make injected modes. If you need to make an injected mode (such as if your mode relies on some grid properties that infused modes can't control), it's probably easier to just make an infused mode, use particular modifiers on it, and then save the result as an injected mode, but if that won't work either for what you're making, then you might want to know how save codes work.

A save code is a text string consisting of a bunch of parts separated by | characters. The first two parts are in plaintext: the first is "@2048PowCompGame" for in-progress games, "@2048PowCompMode" for new games of modes. The second is the version number, which updates whenever some new feature is added to CalcArrays or to the save code format - newer versions of the Compendium can have older save codes loaded, but older versions cannot load newer save codes. So far, every major (first or second version number) update, as well as a few small (third version number) updates have had the save code version increment; there have been a couple of these were nothing outright new was added to CalcArrays or the save code format, but the addition of new special color schemes still forced the save code version increment.

The rest of the parts are in base-64 encoding, either of just a plain string or of the object run through JavaScript's "stringify" function - or, rather, the extended version of stringify described previously with "@BigInt n" and the like.

These save codes include several pieces of data not discussed prior, since they also store data related to global modifiers. Here's the order of the rest of the parts in a mode save code:

An in-progress game save code includes all of those, and then the following additional pieces:

Conclusion

There's plenty more I could say about the code of the 2048 Power Compendium, but this blog post isn't meant to be a total retrospective on the 2048 Power Compendium's development. This blog post is about documenting the behavior of CalcArrays, and giving users enough information to be able to make their own modes to save as save codes and share with others, and with everything I've covered here, I'm hoping I gave a complete enough picture to enable that. There have already been some people who have learned enough about CalcArrays to make injected modes even without this documentation, though usually with some help from me along the way... so hopefully this documentation will make that learning process easier, and help with making infused modes now that those exist!

Return to Website Blog Homepage